package jp.co.nes.awssupport.compile.batch;

import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.bean.CommonContainerBean.ProcessingResult;
import jp.co.nes.awssupport.business.bean.CompileContainerBean;
import jp.co.nes.awssupport.business.bean.CompileInfoBean;
import jp.co.nes.awssupport.business.bean.ManifestBean;
import jp.co.nes.awssupport.business.logic.BillingCsvConvertor;
import jp.co.nes.awssupport.business.logic.BillingCsvReader;
import jp.co.nes.awssupport.business.logic.CompileProgressManage;
import jp.co.nes.awssupport.business.logic.CompileProgressManage.CompletionState;
import jp.co.nes.awssupport.business.logic.DailyCompiledBillingProcessing;
import jp.co.nes.awssupport.business.utility.BillingCsvUtil;
import jp.co.nes.awssupport.business.utility.ManifestHelper;
import jp.co.nes.awssupport.common.base.AbstractActivityTaskClient;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.BeanUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.DbConnUtil;

/**
 * 日次集計結果出力です。<br>
 * なお、本クラスでは、処理途中でコミットを実施しているので、ロールバックが発生した場合でも、
 * それ以前にコミットされているデータは、ロールバックされません。
 */
public class DailyCompiledBilling
        extends AbstractActivityTaskClient<CompileInfoBean, CompileContainerBean<CompileInfoBean>> {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(DailyCompiledBilling.class);

    /** アクティビティのarn(コマンドライン引数で設定) */
    @Option(name = "-aarn", aliases = "--activityArn", usage = "アクティビティのarn", metaVar = "activityArn", required = true)
    private String activityArn;

    /**
     * メインメソッドです。
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        DailyCompiledBilling client = new DailyCompiledBilling();
        client.execute(args);
    }

    /**
     * デフォルトコンストラクタです。
     */
    public DailyCompiledBilling() {
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getActivityArn()
     */
    @Override
    protected String getActivityArn() {
        return activityArn;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getInputClass()
     */
    @Override
    protected Class<CompileInfoBean> getInputClass() {
        return CompileInfoBean.class;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#execute(java.lang.Object)
     */
    @Override
    protected CompileContainerBean<CompileInfoBean> runActivityTask(CompileInfoBean input) {
        String categoryCsv = "BillingCsv";
        String bucketName = AppInfoUtil.getValAndEmptyChk(categoryCsv, "BucketName");
        String objectKey = AppInfoUtil.getValAndEmptyChk(categoryCsv, "ObjectKey");
        String manifestObjectKey = AppInfoUtil.getValAndEmptyChk(categoryCsv, "ManifestObjectKey");

        String category = "DailyCompiledBilling";
        int maxCommit = AppInfoUtil.getIntValAndMinChk(category, "MaxCommitCount", 5000, 1);
        int max = AppInfoUtil.getIntValAndMinChk(category, "BatchExecuteMaxCount", 5000, 1);

        if (logger.isDebugEnabled()) {
            logger.debug(categoryCsv + ",BucketName=" + bucketName);
            logger.debug(categoryCsv + ",ObjectKey(before formatting)=" + objectKey);
            logger.debug(categoryCsv + ",ManifestObjectKey(before formatting)=" + manifestObjectKey);
            logger.debug(category + ",MaxCommitCount=" + maxCommit);
            logger.debug(category + ",BatchExecuteMaxCount=" + max);
        }

        String targetYearMonthValue = input.getTargetYearMonthValue();
        YearMonth targetYearMonth = DateUtil.toYearMonth(targetYearMonthValue);
        String linkedAccountId = input.getLinkedAccountId();
        List<String> payerAccountIdList = getPayerAccountId(bucketName, targetYearMonth, linkedAccountId);
        if (payerAccountIdList.isEmpty()) {
            // 通常発生しない。
            throw new CommonException("payerAccountId is empty");
        }

        List<String> convertNullResultList = new ArrayList<>();
        BillingCsvConvertor bcConvertor = new BillingCsvConvertor(targetYearMonth);
        List<BillingCsvBean> csvList = new ArrayList<>();
        CompileProgressManage cpManage = new CompileProgressManage(targetYearMonth);

        try {
            for (String payerAccountId : payerAccountIdList) {
                String oKey = String.format(objectKey, targetYearMonthValue, payerAccountId, linkedAccountId);
                String moKey = String.format(manifestObjectKey, targetYearMonthValue, payerAccountId);

                ManifestBean manifestBean = getManifestBean(bucketName, moKey);
                try (BillingCsvReader reader = createBillingCsvReader(manifestBean, bucketName, oKey)) {

                    int i = 0;
                    while (reader.readBean()) {
                        i++;

                        if (reader.isHeader()) {
                            // ファイルの1行目の場合
                            continue;
                        }

                        if (reader.isInvalidLine()) {
                            // 分割後のファイルを読み込んでいるので、通常は発生しない。
                            throw new CommonException("Read invalid line");
                        }

                        BillingCsvBean bean = reader.getBean();
                        if (!BillingCsvUtil.isDailyOutput(bean)) {
                            // 日時集計結果に出力しない場合
                            continue;
                        }

                        if (logger.isDebugEnabled()) {
                            int num1 = i % 5000;
                            if (num1 == 0) {
                                logger.debug("read(i=" + i + ")");
                            }
                        }

                        BillingCsvBean convertBean = bcConvertor.convert(bean);
                        if (convertBean == null) {
                            String addValue = createAddValue(bean);
                            if (!convertNullResultList.stream().anyMatch(value -> value.equals(addValue))) {
                                convertNullResultList.add(addValue);
                            }
                        } else {
                            csvList.add(convertBean);
                        }

                        int num2 = i % 10000;
                        if (num2 == 0) {
                            loggingMemory(i);
                        }
                    }
                }
            }

            loggingConvertNullResultList(convertNullResultList);

            logger.debug("grouping start");
            List<BillingCsvBean> groupCsvList = BillingCsvUtil.grouping(csvList);
            logger.debug("grouping end");
            loggingMemory("grouping after");

            // 日次集計結果の削除・登録を実行する。
            // コミットについては、削除後、コミットし、登録については、最大コミット件数ごとにコミットする。
            // よって、処理が失敗した場合は、途中までしかロールバックされない。
            // リカバリについては、本クラスの処理を最初からやり直しする。
            logger.debug("delete start");
            DailyCompiledBillingProcessing dcbProcessing = new DailyCompiledBillingProcessing(targetYearMonth,
                    linkedAccountId);
            dcbProcessing.delete();
            DbConnUtil.commitAll();
            logger.debug("delete end");
            loggingMemory("delete after");

            logger.debug("insert start");
            int commitCount = 0;
            for (BillingCsvBean groupCsvBean : groupCsvList) {
                commitCount++;
                dcbProcessing.add(groupCsvBean);
                if (commitCount >= maxCommit) {
                    logger.debug("insert commit1");
                    dcbProcessing.insert(max);
                    dcbProcessing.clear();
                    DbConnUtil.commitAll();
                    commitCount = 0;
                }
            }
            if (commitCount > 0) {
                logger.debug("insert commit2");
                dcbProcessing.insert(max);
                dcbProcessing.clear();
                DbConnUtil.commitAll();
            }
            logger.debug("insert end");
            loggingMemory("insert after");

            // 正常に日次集計結果の削除・登録が終了した場合、集計進捗状況を完了状態にする。
            // 完了状態にする処理が失敗した場合は、日次集計結果の削除・登録がコミットされた状況になっている。
            // なお、この状況が発生することは少ないと考えている。
            // リカバリについては、単純に再実行すれば問題ない状況である。
            // また、完了状態にする処理が正常に終了した後、何らかの異常が発生し、アプリケーションが異常で終了した場合は、
            // 異常で終了したが、データ(集計進捗状況や日次集計結果の内容)には不整合が発生していない状況である。
            // なお、この状況は、ほとんど発生しないと考えている。
            // リカバリついては、単純に再実行すれば問題ない状況である。
            logger.debug("completion start");
            cpManage.completion(linkedAccountId, CompletionState.DAILY_COMPILED_BILLING);
            logger.debug("completion end");
        } catch (Throwable e) {
            DbConnUtil.rollbackAll();

            // 処理が異常終了した場合、集計進捗状況を失敗状態にする。
            // なお、完了状態にする処理が失敗した場合、処理管理(ProcessControl)で失敗した件数を確認できず、
            // 不明件数にカウントされる。
            logger.debug("failureProcess start");
            cpManage.failureProcess(linkedAccountId);
            logger.debug("failureProcess end");

            throw new CommonException(e);
        } finally {
            DbConnUtil.closeAll();
        }

        // 返却内容の作成
        CompileInfoBean info = new CompileInfoBean();
        BeanUtil.copyProperties(info, input);
        CompileContainerBean<CompileInfoBean> output = new CompileContainerBean<>();
        output.setInfoBean(info);
        output.setProcessingResult(ProcessingResult.SUCCESS);
        loggingMemory("output setting after");
        return output;
    }

    /**
     * 一括請求アカウントIDを返却します。
     *
     * @param bucketName バケット名
     * @param targetYearMonth 処理対象年月
     * @param linkedAccountId 連結アカウントID
     * @return 一括請求アカウントIDリスト
     */
    protected List<String> getPayerAccountId(String bucketName, YearMonth targetYearMonth, String linkedAccountId) {
        return BillingCsvReader.getPayerAccountId(bucketName, targetYearMonth, linkedAccountId);
    }

    /**
     * 請求CSVのマニュフェストを返却します。
     *
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @return 請求CSVのマニュフェスト
     */
    protected ManifestBean getManifestBean(String bucketName, String objectKey) {
        ManifestHelper manifestHelper = new ManifestHelper(bucketName, objectKey);
        return manifestHelper.getManifestBean();
    }

    /**
     * 請求CSVReaderを返却します。
     *
     * @param manifestBean マニュフェスト
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @return 請求CSVReader
     */
    protected BillingCsvReader createBillingCsvReader(ManifestBean manifestBean, String bucketName, String objectKey) {
        return new BillingCsvReader(manifestBean, bucketName, objectKey);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#debugArgument()
     */
    @Override
    protected void debugArgument() {
        if (logger.isDebugEnabled()) {
            logger.debug("activityArn=" + activityArn);
        }
    }

    /**
     * リストに登録する文字列を作成し、返却します。
     *
     * @param bean 請求CSV
     * @return リストに登録する文字列
     */
    private String createAddValue(BillingCsvBean bean) {
        StringBuilder builder = new StringBuilder();
        builder.append(bean.getLineItemType());
        builder.append(":");
        builder.append(bean.getLinkedAccountId());
        builder.append(":");
        builder.append(bean.getProductName());
        builder.append(":");
        builder.append(bean.getUsageType());
        return builder.toString();
    }

    /**
     * 請求CSV変換でnullが返却されたものについてログ出力します。
     *
     * @param convertNullResultList 請求CSV変換でnullが返却されたリスト
     */
    private void loggingConvertNullResultList(List<String> convertNullResultList) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < convertNullResultList.size(); i++) {
            if (i == 0) {
                builder.append("Convert Null Result Info [lineItemType:linkedAccountId:productName:usageType=");
            }
            builder.append(convertNullResultList.get(i));
            if (i == convertNullResultList.size() - 1) {
                builder.append("]");
            } else {
                builder.append(",");
            }
        }
        String value = builder.toString();
        if (!StringUtils.isEmpty(value)) {
            logger.debug(builder.toString());
        }
    }

    /**
     * ヒープサイズの状況をログ出力します。
     *
     * @param lineCount 行数
     */
    private void loggingMemory(int lineCount) {
        loggingMemory("lineCount=" + lineCount);
    }

    /**
     * ヒープサイズの状況をログ出力します。
     *
     * @param preMessage ログの先頭に出力する内容
     */
    private void loggingMemory(String preMessage) {
        long totalMemory = Runtime.getRuntime().totalMemory();
        long freeMemory = Runtime.getRuntime().freeMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        StringBuilder builder = new StringBuilder();
        if (preMessage != null) {
            builder.append(preMessage);
            builder.append(",");
        }
        builder.append("maxMemory(KB)=");
        builder.append(maxMemory / 1024);
        builder.append(",totalMemory(KB)=");
        builder.append(totalMemory / 1024);
        builder.append(",freeMemory(KB)=");
        builder.append(freeMemory / 1024);
        builder.append(",useMemory(KB)(totalMemory - freeMemory)=");
        builder.append((totalMemory - freeMemory) / 1024);
        logger.info(builder.toString());
    }
}
